#!/usr/local/bin/lua
-- dm - the Darwin binary package installer
-- Stuart Crook, 27/3/09

require "luasql.sqlite3";
require "posix";

function usage() 
	print("dm: the Darwin binary package manager");
	print("  dm --list { installed | available | missing | all | groups | systems }");
	print("     --info { package | group | system }");
	print("     --add [ options ] { package | group | system }");
	print("     --remove [ options ] { package | group | system }");
	print("     --get [ setting [ setting ... ] ]");
	print("     --set setting value [ setting value ] [ ... ]");
	print("     --update [ package | group | system ]");
	print("     --version");
end

--
-- various globals
--
i = 0; 		-- used globally to iterate through the arg table
tprefs = {};	-- temporary prefs read from the command line

-- local and remote paths
dm_config_path = '/etc/dm.config';
pdpd_path = '/var/db/pdpd';
darwinup_db_path = '/.DarwinDepot/Database-V100';
darwinup_path = '/usr/local/sbin/darwinup';
core_versions_path = '/etc/core_versions';

version_major = 1;
version_minor = 0;
version_url = 'http://puredarwin.googlecode.com/svn/Roots/pd/versions';
pdpd_url = 'http://puredarwin.googlecode.com/svn/Roots/pd/pdpd';
dm_url = '';

-- default preference values
prefs = {};
	prefs['depot'] = nil;			-- local package file cache
	prefs['cache'] = '/tmp/';	-- temporary download directory
	prefs['lang'] = 'en';
	prefs['findDeps'] = 'Y';	-- find and install/uninstall dependencies
	prefs['download'] = 'Y';	-- should files be downloaded? or just listed
	prefs['install'] = 'Y';		-- should packages actually be installed?
	prefs['uninstall'] = 'Y';	-- should packages actually be uninstalled?
	prefs['cleanup'] = 'Y';			-- remove downloaded files after installing

-- preference names and functions to validate their setting
f_yes_no = function(s) return string.match(s,'^[yYnN]$'); end;
f_is_dir = 	function(s)
							local st = posix.stat(s);
							if st and st.type == 'directory' then return true; end
							return false;							
						end

prefval = {};
	-- depot and cache should match to paths
	prefval['depot'] = f_is_dir;
	prefval['cache'] = f_is_dir;
	prefval['lang'] = function(s) return string.match(s,'^%l%l$'); end;
	prefval['findDeps'] = f_yes_no;
	prefval['download'] = f_yes_no;
	prefval['install'] = f_yes_no;
	prefval['uninstall'] = f_yes_no;
	prefval['cleanup'] = f_yes_no;
	

-- the remote depots, in search order
depots = {	'http://code.google.com/p/puredarwin/source/browse/Roots/pd/',
						'http://code.google.com/p/puredarwin/source/browse/Roots/X/',
						'http://code.google.com/p/puredarwin/source/browse/Roots/9G55pd1/',
						'http://code.google.com/p/puredarwin/source/browse/Roots/9F33pd1/',
						'http://code.google.com/p/puredarwin/source/browse/Roots/9D34/',
						'http://code.google.com/p/puredarwin/source/browse/Roots/9A581/',
						'http://src.macosforge.org/Roots/9G55/',
						'http://src.macosforge.org/Roots/9F33/',
						'http://src.macosforge.org/Roots/9E17/',
						'http://src.macosforge.org/Roots/9D34/',
						'http://src.macosforge.org/Roots/9C31/',
						'http://src.macosforge.org/Roots/9B18/',
						'http://src.macosforge.org/Roots/9A581/' };

-- we expect more args, so complain if there aren't any
function more_args()
	i = i+1;
	if i > #arg then
		usage();
		os.exit();
	end
	return true;
end



-- DATABASE FUNCTIONS --

-- create the global database connection
function open_db()
	dbenv = luasql.sqlite3();
	if dbenv == nil then
		print('Error: could not create database environment');
		os.exit();
	end
	if posix.stat(pdpd_path) == nil then
		print('Error: could not find PureDarwin Package Database. Please run "dm --update"');
		os.exit();
	end
	db = dbenv:connect(pdpd_path);
	if db == nil then
		print('Error: could not create database connection');
		os.exit();
	end
end

-- neatly close the dbs again
function close_db()
	db:close();
	dbenv:close();
end


-- going to try something fancy with closures
function pack_name(t)
	return function(name,row)
		table.insert(t,name);
	end
end

function pack_details(t)
	return function(name,row)
		t[name] = row;
	end
end

-- actually, when throwing whole rows around is this easy, I
-- don't think we actually need this one
--function pack_id(t)
--	return function(name,row)
--		t[name] = row.id;
--	end
--end

-- create a connection to the darwinup db and read the values 
-- from it, passing them to closure f to store them as needed
function read_darwinup(f)
	local name,version;
	local st = posix.stat(darwinup_db_path);
	if st then
		local du = dbenv:connect(darwinup_db_path);
		if du then
			local cur = du:execute('SELECT * FROM archives WHERE active=1 AND info=0 ORDER BY serial');
			if cur then
				local row = cur:fetch({},'a');
				while row do
					-- extract name and version info from the installed file name
					name,version = string.match(row.name,'([%w_.%-]*)__([%d?.]*).root.tar.gz$');
					if name and version then 
						row.name = name;
						row.version = version;
						f(name,row) 
					end
					row = cur:fetch({},'a');
				end
				cur:close();
			end
			du:close();
		end
	end
end

-- read the members of the group with the given id and pass to
-- closure f for storage
function read_members(id,f)
	cur = db:execute(string.format('SELECT * FROM packages JOIN members ON packages.id=members.package_id WHERE members.group_id=%d', id));
	if cur then
		row = cur:fetch({},'a');
		while row do
			f(row.name,row);
			row = cur:fetch({},'a');
		end
		cur:close();
	end
end

-- read a list of the groups which make up a system
function read_system_groups(id,f)
	cur = db:execute(string.format('SELECT * FROM groups JOIN sys_groups ON groups.id=sys_groups.group_id WHERE sys_groups.system_id=%d',id));
	if cur then
		row = cur:fetch({},'a');
		while row do
			f(row.name,row);
			row = cur:fetch({},'a');
		end
		cur:close();
	end
end

-- read a list of the loose packages from a system
function read_system_packages(id,f)
	cur = db:execute(string.format('SELECT * FROM packages JOIN sys_packages ON packages.id=sys_packages.package_id WHERE sys_packages.system_id=%d',id));
	if cur then
		row = cur:fetch({},'a');
		while row do
			f(row.name,row);
			row = cur:fetch({},'a');
		end
		cur:close();
	end
end

-- composite shortcut function to read in all installed packages
function read_installed(f)
	-- read the core_versions list first, because darwinup may over-write it
	local r = read_file(core_versions_path);
	if r then
		local name,version;
		local l = r:next();
		while l do
			name,version = string.match(l,'(%S*)=(%S*)');
			if name and version then
				if version == '' then version='???' end
				f(name,{name=name,version=version});
			end
			l = r:next();
		end
		r:done();
	end
	-- now read installed files from darwinup
	read_darwinup(f);
end

-- read all packages and pass them to function f for storage
function read_all(table,f)
	local cur = db:execute(string.format('SELECT * FROM %s',table));
	if cur then
		local row = cur:fetch({},'a');
		while row do
			f(row.name,row);
			row = cur:fetch({},'a');
		end
		cur:close();
	end
end

-- read packages depending on their availability and pass them
-- to the closure f for storage
function read_avail(avail,f)
	if avail then avail = '1' else avail = '0' end
	cur = db:execute(string.format('SELECT * FROM packages WHERE available=%d',avail));
	if cur then
		row = cur:fetch({},'a');
		while row do
			f(row.name,row);
			row = cur:fetch({},'a');
		end
		cur:close();
	end
end

-- look up the record for name in the given table, passing the result
-- to f to store and returning true or false depending on success
function read_name(table,name,t)
	r = false;
	cur = db:execute(string.format('SELECT * FROM %s WHERE name="%s"',table,name));
	if cur then
		t = cur:fetch(t,'a');
		if t then r = true end
		cur:close();
	end
	return r;
end

-- look up the record for name in the given table, passing the result
-- to f to store and returning true or false depending on success
function read_id(table,id,t)
	r = false;
	cur = db:execute(string.format('SELECT * FROM %s WHERE id=%d',table,id));
	if cur then
		t = cur:fetch(t,'a');
		if t then r = true end
		cur:close();
	end
	return r;
end

-- read the description for the given package, group or system
function read_desc(table,id)
	r = '[no description available]';
	cur = db:execute(string.format('SELECT * FROM %s_descs WHERE %s_id=%d AND language="%s"',table,table,id,prefs.lang));
	if cur then
		row = cur:fetch({},'a');
		if row then r = row.desc end
		cur:close();
	end
	return r;
end

-- look up dependencies for the given package and pass them to
-- function f for storing
function read_dependencies(id,f)
	cur = db:execute(string.format('SELECT * FROM packages JOIN deps ON packages.id=deps.p2_id WHERE deps.p1_id=%d', id));
	if cur then
		row = cur:fetch({},'a');
		while row do
			f(row.name,row);
			row = cur:fetch({},'a');
		end
		cur:close();
	end
end

-- print a list of packages, adding status info drawn from the
-- missing (m) and installed (n) dictionaries
function list_packages(p,m,n)
	local status,i,v;
	table.sort(p);
	for i,v in pairs(p) do
		if n[v] then status = string.format('[v%s installed]',n[v].version);
		elseif m[v] then status = '[missing]'
		else status = '' end
		print(string.format('  %s %s',v,status)); -- 2 space indent :)
	end
end

-- add name to dictionary p, splitting it out into component
-- packages if it is a group, system, or package list
function add_package(name,p)
		local t = {};
		-- is it a package?
		if read_name('packages',name,t) then p[name] = t;
		-- or a group?
		elseif read_name('groups',name,t) then add_group(name,t,p);
		-- or a system?
		elseif read_name('systems',name,t) then add_system(name,t,p);
		-- or maybe a file?
		else add_package_list(name,p) end
end

-- add every member of group name (which is described by table t)
-- to dictionary p
function add_group(name,t,p)
	local g = {}; -- array of package names in this group
	local i,v;
	read_members(t.id,pack_name(g));
	for i,v in ipairs(g) do
		add_package(v,p); -- pdpdmake should ensure these are only packages
	end
end

-- add every member of system name (which is described by table t)
-- to p, expanding groups as necessary
function add_system(name,t,p)
	local g = {}; -- general local listage
	local i,k,v;
	-- get a list of all groups for this system
	read_system_groups(t.id,pack_details(g));
	for k,v in pairs(g) do
		add_group(k,v,p);
	end
	-- get a list of all loose packages for the system
	g = {};
	read_system_packages(t.id,pack_name(g));
	for i,v in ipairs(g) do
		add_package(v,p);
	end
end

-- attempt to add the contents of the file named name to the
-- dictionary of packages p, calling add_package for each line
function add_package_list(name,p)
	local f = nil; -- to hold anonymous reader function
	if string.match(name,"^https?://.*") then -- sloppy, I know
		print('Looks like a url');
		f = read_url(name);
	else
		f = read_file(name);
	end
	if f then
		name = f:next();
		while name do
			add_package(name,p);
			name = f:next();
		end
		--print('----');
		f:done();
	end
end

-- create a list-reading closure around a file descriptor
function list_reader(fd)
	local f = {};
	-- function to return next name in list
	function f:next()
		local l = fd:read();
		while l do
			l = string.match(l,'^([^#%s]%S*)$');
			if l then return l; end
			l = fd:read();
		end
		return nil;
	end
	-- function to close the file handle when done
	function f:done()
		fd:close();
	end
	return f;
end

-- attempt to open a file and return a function which reads
-- a line and returns the first part which each invocation
function read_file(name)
	local fd = io.open(name);
	if fd then return list_reader(fd); end
	return nil;
end

-- attempt to read a text file from a remote host and return
-- a function which reads a line with every invocation
function read_url(url)
	-- should really confirm that url _is_ a url
	local fd = io.popen(string.format('curl -f -s %s',url));
	if fd then return list_reader(fd); end
	return nil;
end


-- THE MAIN COMMAND MODES --

--
-- list information about packages or groups
--
function list()
	local k,v;
	setup_prefs();
	open_db();
	p = {}; -- to hold the packages
	m = {}; -- to hold missing packages (optional)
	n = {}; -- to hold installed packages (optional)
	i = i+1;

	-- list installed packages
	if i > #arg or arg[i] == 'installed' then
		read_installed(pack_details(n));
		for k,v in pairs(n) do table.insert(p,k); end

	-- list all packages
	elseif arg[i] == 'all' then
		read_all('packages',pack_name(p));
		read_installed(pack_details(n));
		read_avail(false,pack_details(m));

	-- list available packages
	elseif arg[i] == 'available' then
		read_avail(true,pack_name(p));

	-- list missing packages
	elseif arg[i] == 'missing' then
		read_avail(false,pack_name(p));

	-- list all systems
	elseif arg[i] == 'systems' then
		read_all('systems',pack_name(p));

	-- list all groups
	elseif arg[i] == 'groups' then
		read_all('groups',pack_name(p));
	end

	-- print whatever is in p, checking its status
	list_packages(p,m,n);

	close_db();
	os.exit();
end

--
-- display information about the given package, group or system
--
function info()
	setup_prefs();
	open_db();
	t = {}; -- where we will store info about the target

	-- is it a package?
	if read_name('packages',arg[i],t) then
		-- get the installed list...
		n = {};
		read_installed(pack_details(n));
		-- ...then work out the package's status
		if n[t.name] then status = 'installed'
		elseif t.available == '0' then status = 'missing'
		else status = 'available' end

		-- get the package's dependencies
		d = {};
		read_dependencies(t.id,pack_name(d));
		
		-- and display the information
		print('   type: package');
		print(string.format('   name: %s',t.name));
		print(string.format(' status: %s',status));
		print(string.format('version: %s',t.version));
		print(string.format('description:\n  %s',read_desc('package',t.id)));
		print(string.format('%d dependencies',#d));
		if #d then
			m = {};
			read_avail(false,pack_details(m));
			list_packages(d,m,n);
		end
		print(string.format('%d unresolved dependencies',t.unres));

	-- or a group?
	elseif read_name('groups',arg[i],t) then
		p = {};
		read_members(t.id,pack_name(p));

		-- display the information
		print('   type: group');
		print(string.format('   name: %s',t.name));
		print(string.format('description:\n\t%s',read_desc('group',t.id)));
		print(string.format('%d members:',#p));
		if #p ~= 0 then
			m = {}; n = {};
			read_installed(pack_details(n));
			read_avail(false,pack_details(m));
			list_packages(p,m,n);
		end

	-- or a system?
	elseif read_name('systems',arg[i],t) then

		print('   type: system');
		print(string.format('   name: %s',t.name));


	-- or something else?
	else
 		print(string.format("'%s' is not a recognised package, group or system name.",arg[i]));
	end

	close_db();
	os.exit();
end

-- add the package described by t and all of its (available 
-- but not-installed) dependencies to the list d, checking
-- installed list n
function add_dependants(t,d,n)
	local l = {};		-- local storage for dependants list
	local k,v;
	--print('add_dependents:',t.name);

	-- check that we haven't already met the package...
	if d[t.name] == nil then
		-- ...that it is available...
		if t.available == '0' then
			--print(string.format('package %s is not available',t.name));
		-- ...and not already installed...
		elseif n[t.name] then
			--print(string.format('package %s is already installed',t.name));
		else
			d[t.name] = t;	-- add to list of packages to install
			if prefs.findDeps then
				read_dependencies(t.id,pack_details(l));
				for k,v in pairs(l) do
					--print(string.format('%s has a dependency on %s',t.name,k));
					add_dependants(v,d,n);
				end
			end -- if prefs.findDeps
		end -- available or installed
	end -- not already in the 'to install' list
end

-- Check whether file exists in dir, returning either nil or
-- the full file path
function check_file(path,name)
	local fname = path..name;
	--print('checking:',fname);
	local s = posix.stat(fname);
	if s and s.type == 'regular' and s.size > 0 then
		return fname;
	end
	return nil;
end

-- attempt to download file fname from site url and store it
-- in download cache directory cname
function download(url,fname,cname)
	local furl = url..fname;
	local cdir = cname..fname;
	--print(string.format('Downloading %s',fname));
	os.execute(string.format('curl -# -S -f --create-dirs -o %s %s',cdir,furl));
end

-- install all of the packages in p (if prefs.install == true), 
-- downloading them if needed, checking their sha1 hashes
function install_packages(p)
	local i,k,v,w;
	local f = {}; -- found package list, because we're splitting
								-- the process into two separate stages

	-- locate, identify and stage the packages
	for k,v in pairs(p) do
		local pname = k..'.root.tar.gz'; -- full package filename
		local fname = nil;

		-- are we using a local depot directory?
		if prefs.depot then 
			fname = check_file(prefs.depot,pname); 
		end

		-- if that didn't work, check the download cache
		if fname == nil then
			fname = check_file(prefs.cache,pname);
			if fname then
				print(string.format('Found a locally cached version of %s',pname));
			else
				print(string.format('Downloading %s...',pname));
				for i,w in ipairs(depots) do
					download(w,pname,prefs.cache);
					fname = check_file(prefs.cache,pname);
					if fname then break end
					print("\b");
				end
			end
		end

		if fname == nil then
			print(string.format('Could not find package %s',k));
		else
			-- check to see if this package is valid
			local h = hash_file(fname);
			if h then
				if h == v.hash then
					-- this is the version of the file pdpd knows about
					pname = prefs.cache..k..'__'..v.version..'.root.tar.gz';
				else
					-- hmm
					pname = prefs.cache..k..'__???'..'.root.tar.gz';
				end
				-- rename the package and add it to the final install list
				os.rename(fname,pname);
				table.insert(f,pname);
			else
				-- if we can't hash the file then we're not touching it
			end
		end
	end

	-- the install phase...
	if prefs.install then
		-- install packages through darwinup
		for i,v in ipairs(f) do
			os.execute(string.format('%s install %s > /dev/null',darwinup_path,v));
		end

		-- remove the temporary files
		if prefs.cleanup then
			for i,v in ipairs(f) do 
				os.remove(v); 
			end
		end
	end

end

-- call 'openssl sha1' on filename and return its hash value
function hash_file(fname)
	local h = nil;
	local fd = io.popen(string.format('openssl sha1 %s',fname));
	if fd then
		local l = fd:read();
		if l then
			l = string.match(l,' ([%dabcdef]*)');
			if l then h=l; end
		end
		fd:close();
	end
	return h;
end

--
-- add the specified packages, groups or systems
--
function add()
	setup_prefs();
	open_db();
	p = {}; -- package list (dictionary)

	-- read the remaining arguments, adding them to p
	repeat 
		add_package(arg[i],p);
		i = i+1;
	until i > #arg

	-- see if the args yielded any valid packages
	if next(p) then
		d = {}; n = {};
		read_installed(pack_details(n));
		for k,v in pairs(p) do
			add_dependants(v,d,n);
		end

		if next(d) then
			-- prefs.download == false kills all further steps
			if not prefs.download then
				print('These packages would be installed:');
				for k,v in pairs(d) do
					print(string.format('  %s [v%s]',k,v.version));
				end
			else
				install_packages(d);
			end
		else
			print('No packages to be installed');
		end
	else
		print('Error: No valid package names were given');
	end -- if next(p) then above

	close_db();
	os.exit();
end

--
-- remove the specified packages (groups or systems?)
--
function remove()
	setup_prefs();
	open_db();

	print('Sorry, no removing at the moment');

	close_db();
	os.exit();
end

-- setup preferences, layering temp on dm.config on defaults
function setup_prefs()
	load_prefs(prefs);
	local k,v;
	-- copy command line tprefs into main prefs dictionary
	for k,v in pairs(tprefs) do prefs[k]=v end
	-- adjust main prefs dictionary so results are easier
	for k,v in pairs(prefs) do
		if v=='y' or v=='Y' then prefs[k]=true;
		elseif v=='n' or v=='N' then prefs[k]=false;
		elseif string.match(v,'^%d*$') then prefs[k]=tonumber(v); end
	end
end

-- load preferences from /etc/dm.config into dictionary p
function load_prefs(p)
	local f = read_file(dm_config_path);
	if f then
		local a,b;
		local l = f:next();
		while l do
			a,b = string.match(l,"(%S*)=(%S*)");
			if a and b and prefval[a] and prefval[a](b) then p[a]=b end
			l = f:next();
		end
		f:done();
	end
end

-- save preferences from dictionary p out to /etc/dm.config
function save_prefs(p)
	local fd, err = io.open(dm_config_path,'w+');
	if fd then
		print('Saving preferences!');
		local k,v;
		for k,v in pairs(p) do
			fd:write(string.format("%s=%s\n",k,v));
		end
		fd:close();
	else
		print(string.format("Couldn't open config file: %s",err));
	end
end

--
-- display the current values of user defaults
--
function get()
	load_prefs(prefs); -- overlay dm.config on the default prefs
	local f = function(k,v) print(string.format('  %s\t= %s',k,v)); end

	-- if there were no more args, list all current settings
	if i == #arg then
		for k,v in pairs(prefs) do f(k,v); end
	else
		i = i+1;
		repeat 
			if prefs[arg[i]] then f(arg[i],prefs[arg[i]]); end
			i = i+1;
		until i > #arg
	end
	os.exit();
end

--
-- set the value of user defaults and write them to the config file
--
function set()
	local lprefs = {};	-- we ignore the prefs table because we don't
	load_prefs(lprefs); -- want to store out all the default values
	
	while i+1 <= #arg do
		if prefval[arg[i]] then
			if prefval[arg[i]](arg[i+1]) then
				lprefs[arg[i]] = arg[i+1];
				print(string.format("%s set to '%s'",arg[i],arg[i+1]));
			else
				print(string.format("'%s' is not a valid value for %s",arg[i+1],arg[i]));
			end
		else
			print(string.format("'%s' is not a recognised setting",arg[i]));
		end
		i = i+2;
	end

	save_prefs(lprefs);
	os.exit();
end

-- read version information into a dictionary
function read_versions(vs)
	local r,l,a,b;
	r = read_url(version_url);
	if r then
		l = r:next();
		while l do
			a,b = string.match(l,'([%w_]*)=(%w*)');
			if a and b then vs[a]=b; end
			l = r:next();
		end
		r:done();
		return true;
	end
	return false;
end

-- retrieve the current version number from the database
function db_version()
	local v = 0;
	local st = posix.stat(pdpd_path);
	if st then
		open_db();
		local cur = db:execute('SELECT version FROM db_info');
		if cur then
			local row = cur:fetch({},'a');
			v = tonumber(row.version);
			cur:close();
		end
		close_db();
	end
	return v;
end

-- update the db, downloading it to /tmp/dm/ and then
-- moving it into place
function update_db(hash)
	local tname = prefs.cache..'pdpd';
	-- clear the destination, just in case
	if posix.stat(tname) then os.remove(tname); end
	-- copy in the new database file
	print('Downloading PureDarwin Package Database');
	os.execute(string.format('curl -# -f -o %s %s',tname,pdpd_url));
	-- check that that worked
	if posix.stat(tname) then 
		local h = hash_file(tname);
		if h == hash then
			os.rename(tname,pdpd_path);
			print('Updated the PureDarwin Package Database');
		else
			print('Failed to verify the authenticity of the PureDarwin Package Database.');
		end
	end
end

--
-- query somewhere to see what needs updating
--
function update()
	local vs = {};
	m = {}; n = {}; p = {}; q = {}; -- tables are cheap, right?
	setup_prefs();

	-- check the latest version of the tool and database
	if read_versions(vs) then
		if tonumber(vs.pdpd_version) > db_version() then
			update_db(vs.pdpd_hash);
		end
	end

	-- now we've done that, see if we came here for something else
	if i ~= #arg then
		open_db();
		i = i+1;
		read_installed(pack_details(n));
		
		-- build a list of all installed packages...
		if arg[i] == 'all' then
			local k,v;
			for k,v in pairs(n) do table.insert(p,k); end
		-- ...or just the ones specified
		else
			repeat
				if n[arg[i]] then table.insert(p,arg[i]); end
				i = i+1;
			until i > #arg;
		end

		-- if that left up with anything, see if we've got updates
		if next(p) then
			local i,v;
			read_all('packages',pack_details(m));
			-- check installed version against database-listed version
			for i,v in ipairs(p) do
				print(v,n[v].version,m[v].version);
				-- if a package needs upgrading, add it's latest info
				if n[v].version ~= m[v].version then q[v]=m[v]; end
			end

			-- so do we now have anything to install?
			if next(q) then
				if not prefs.download then
					print('These packages have updates available');
					table.sort(q);
					for k,v in pairs(q) do 
						print(string.format('  %s [v%s -> v%s]',k,n[v].version,v.version)); 
					end
				else
					install_packages(q);
				end
			else
				-- all installed packages are up to date
				print('All packages are up to date');
			end
		else
			-- user didn't name any installed packages
			print('No packages to update');
		end


		close_db();
	end
	os.exit();
end


--
-- Finally, the beginning...
--
-- check that we are running as admin
pw = posix.getpasswd();
if pw.uid ~= 0 then
	print('Error: Admin privileges required to run');
	os.exit();
end

-- check that we got at least one argument
more_args();

-- process the args
repeat
	local opt;

	-- the main command mode switches
	if arg[i] == '--list' then list();
	elseif arg[i] == '--info' and more_args() then info();
	elseif arg[i] == '--add' and more_args() then add();
	elseif arg[i] == '--remove' and more_args() then remove();
	elseif arg[i] == '--get' then get();
	elseif arg[i] == '--set' and more_args() then set();
	elseif arg[i] == '--update' then update();
	elseif arg[i] == '--version' then 
		print(string.format('%d.%d',version_major,version_minor));
		os.exit();
	-- the temporary option switches
	else
		opt = string.match(arg[i],"(%w*)");
		if opt and prefval[opt] and more_args() then
			if prefval[opt](arg[i]) then
				--print('setting',opt,'to',arg[i]);
				tprefs[opt] = arg[i];
			else
				print(string.format("'%s' is not a valid value for %s",arg[i],opt));
			end
		else
			print(string.format("Warning: Unrecognised argument '%s'",arg[i]));
		end
	end

	i = i+1;
until i > #arg

-- incase we fell through to here without doing anything
usage();


